﻿/*
   Copyright 2013 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using Flower.Client;
using Flower.Logging;
using Flower.Workflow;
using Spring.Aop;
using Spring.Aop.Framework;
using Spring.Context;
using Spring.Context.Support;
using Spring.Objects.Factory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Threading.Tasks;

namespace Flower.Services.Host
{
    internal class ServiceHostManager : MarshalByRefObject, IServiceHostManager
    {
        private static readonly Log Log = LogManager.CreateLog(typeof(ServiceHostManager).Name);

        private class InstanceProvider : IInstanceProvider
        {
            private static readonly Type[] EmptyTypesArray = new Type[0];
            private static readonly object[] EmptyObjectsArray = new object[0];

            private readonly IFlowerClient _flowerClient;

            public InstanceProvider(IFlowerClient flowerClient)
            {
                _flowerClient = flowerClient;
            }

            public object GetInstance(InstanceContext instanceContext, System.ServiceModel.Channels.Message message)
            {
                return InitService(GetInstanceByType(instanceContext.Host.Description.ServiceType));
            }

            public object GetInstance(InstanceContext instanceContext)
            {
                return InitService(GetInstanceByType(instanceContext.Host.Description.ServiceType));
            }

            public void ReleaseInstance(InstanceContext instanceContext, object instance)
            {
                ReleaseService(instance);
            }

            public object GetInstanceByType(Type type)
            {
                return type.GetConstructor(EmptyTypesArray).Invoke(EmptyObjectsArray);
            }

            public object InitService(object service)
            {
                Type serviceType = service.GetType();

                foreach (MethodInfo method in serviceType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
                {
                    if (method.GetCustomAttribute<InitAttribute>() != null)
                    {
                        var args = new List<object>();
                        var methParams = method.GetParameters();

                        foreach (ParameterInfo param in methParams)
                        {
                            if (param.ParameterType == typeof(IFlowerClient))
                            {
                                args.Add(_flowerClient);
                            }
                            else
                            {
                                var serviceAttr = param.GetCustomAttribute<ServiceAttribute>();

                                if (serviceAttr != null)
                                {
                                    var resolvedService = _flowerClient.GetService<object>(serviceAttr.ServicePath);

                                    if (param.ParameterType.IsAssignableFrom(resolvedService.GetType()))
                                    {
                                        args.Add(resolvedService);
                                    }
                                    else
                                    {
                                        throw new ServicesProviderException
                                        (
                                            string.Format
                                            (
                                                "Failed to resolve the service {0} for the parameter {1} of the method {2}.{3}. Service doesn't implement the interface requested. The service implements {4}.",
                                                param.ParameterType.AssemblyQualifiedName,
                                                param.Name,
                                                serviceType.FullName,
                                                method.Name,
                                                string.Join(", ", resolvedService.GetType().GetInterfaces().Select(it => it.AssemblyQualifiedName))
                                            )
                                        );
                                    }
                                }
                                else
                                {
                                    throw new ServicesProviderException
                                    (
                                        string.Format
                                        (
                                            "Failed to resolve the service {0} for the parameter {1} of the method {2}.{3}. Service path is not specified.",
                                            param.ParameterType.FullName,
                                            param.Name,
                                            serviceType.FullName,
                                            method.Name
                                        )
                                    );
                                }
                            }
                        }

                        if (methParams.Length == args.Count)
                        {
                            method.Invoke(service, args.ToArray());
                        }
                    }
                }

                return service;
            }

            public void ReleaseService(object service)
            {
                Type serviceType = service.GetType();

                foreach (MethodInfo method in serviceType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
                {
                    if (method.GetCustomAttribute<ReleaseAttribute>() != null)
                    {
                        if (method.GetParameters().Length == 0)
                        {
                            method.Invoke(service, new object[0]);
                        }
                        else
                        {
                            Log.Warning
                            (
                                string.Format
                                (
                                    "Failed to invoke stop method of the service {0}. The method must be parameterless.",
                                    serviceType.FullName
                                )
                            );
                        }
                    }
                }
            }
        }

        private class InstanceProviderBehavior : IEndpointBehavior
        {
            private readonly IFlowerClient _flowerClient;

            public InstanceProviderBehavior(IFlowerClient flowerClient)
            {
                _flowerClient = flowerClient;
            }

            public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
            {
            }

            public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
            }

            public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
            {
                endpointDispatcher.DispatchRuntime.InstanceProvider =
                    new InstanceProvider(_flowerClient);
            }

            public void Validate(ServiceEndpoint endpoint)
            {
            }
        }

        private string _configurationPath;
        private ServiceHost _serviceHost;
        private IFlowerClient _flowerClient;
        private object _singletonInstance;

        public bool Start(string configurationPath)
        {
            try
            {
                _configurationPath = configurationPath;

                IApplicationContext appContext =
                    new XmlApplicationContext(AppDomain.CurrentDomain.BaseDirectory + @"\Flower.Services.Host.config");

                try
                {
                    _flowerClient = appContext.GetObject("flowerClient") as IFlowerClient;

                    if (_flowerClient == null)
                    {
                        Log.Error("An object 'flowerClient' doesn't implement IFlowerClient.");
                        return false;
                    }
                }
                catch (NoSuchObjectDefinitionException)
                {
                    Log.Error("An object 'flowerClient' is not defined in Flower.Services.Host.config.");
                    return false;
                }

                var serviceHostConfiguration =
                    _flowerClient.GetService<ServiceHostConfiguration>(configurationPath);

                if (serviceHostConfiguration.Service == null && serviceHostConfiguration.ServiceType == null)
                {
                    Log.Error(string.Format("Service host configuration {0} has neither service nor service type defined.", configurationPath));
                    return false;
                }

                if (serviceHostConfiguration.Endpoints == null || serviceHostConfiguration.Endpoints.Count == 0)
                {
                    Log.Error(string.Format("Service host configuration {0} has no endpoints defined.", configurationPath));
                    return false;
                }

                Uri[] baseAddresses =
                    (serviceHostConfiguration.BaseAddresses != null) ?
                        serviceHostConfiguration.BaseAddresses.Cast<string>().Select(uri => new Uri(uri)).ToArray() :
                        new Uri[0];

                if (serviceHostConfiguration.Service != null)
                {
                    var instanceProvider = new InstanceProvider(_flowerClient);
                    _singletonInstance = instanceProvider.InitService(serviceHostConfiguration.Service);
                    _serviceHost = 
                        new ServiceHost
                        (
                            _singletonInstance,
                            baseAddresses
                        );
                }
                else
                {
                    var serviceType = Type.GetType(serviceHostConfiguration.ServiceType);
                    if (serviceType == null)
                    {
                        Log.Error(string.Format("Failed to load type {0}. Type not found.", serviceHostConfiguration.ServiceType));
                        return false;
                    }

                    if (serviceType.GetConstructor(new Type[0]) == null)
                    {
                        Log.Error(string.Format("The type {0} cannot be used as a service implementation because it has no parameterless constructor.", serviceHostConfiguration.ServiceType));
                        return false;
                    }

                    _serviceHost = new ServiceHost(serviceType, baseAddresses);
                }

                if (serviceHostConfiguration.Behaviors != null)
                {
                    foreach (IServiceBehavior behavior in serviceHostConfiguration.Behaviors)
                    {
                        _serviceHost.Description.Behaviors.Add(behavior);
                    }
                }

                int i = 0;
                foreach (ServiceEndpointConfiguration endpointConfig in serviceHostConfiguration.Endpoints)
                {
                    if (string.IsNullOrEmpty(endpointConfig.Contract))
                    {
                        Log.Error(string.Format("Endpoint {0} of the service host configuration {1} has no contract defined.", i, configurationPath));
                        continue;
                    }

                    if (endpointConfig.Binding == null)
                    {
                        Log.Error(string.Format("Endpoint {0} of the service host configuration {1} has no binding defined.", i, configurationPath));
                        continue;
                    }

                    if (string.IsNullOrEmpty(endpointConfig.Address))
                    {
                        Log.Error(string.Format("Endpoint {0} of the service host configuration {1} has no address defined.", i, configurationPath));
                        continue;
                    }

                    var endpoint = _serviceHost.AddServiceEndpoint
                    (
                        endpointConfig.Contract,
                        endpointConfig.Binding,
                        endpointConfig.Address,
                        (endpointConfig.ListenUri != null) ? new Uri(endpointConfig.ListenUri) : null
                    );

                    if (serviceHostConfiguration.Service == null)
                    {
                        endpoint.EndpointBehaviors.Add(new InstanceProviderBehavior(_flowerClient));
                    }

                    if (endpointConfig.Behaviors != null)
                    {
                        foreach (IEndpointBehavior behaviour in endpointConfig.Behaviors)
                        {
                            endpoint.EndpointBehaviors.Add(behaviour);
                        }
                    }

                    ++i;
                }

                _serviceHost.Open();

                return true;
            }
            catch (Exception ex)
            {
                Log.Error(string.Format("Failed to start service host {0}.", configurationPath), ex);
                return false;
            }
        }

        public void Stop()
        {
            try
            {
                if (_singletonInstance != null)
                {
                    var instanceProvider = new InstanceProvider(_flowerClient);
                    instanceProvider.ReleaseService(_singletonInstance);
                }

                _serviceHost.Close();
            }
            catch (Exception ex)
            {
                Log.Error(string.Format("Failed to stop service host {0}.", _configurationPath), ex);
            }
        }
    }
}
